8.2.1 log包
让我们从 log
包提供的最基本的功能开始,之后再学习如何创建定制的日志记录器。记录日志的目的是跟踪程序什么时候在什么位置做了什么。这就需要通过某些配置在每个日志项上要写的一些信息,如代码清单8-2所示。
代码清单8-2 跟踪日志的样例
TRACE: 2009/11/10 23:00:00.000000 /tmpfs/gosandbox-/prog.go:14: message
在代码清单8-2中,可以看到一个由 log
包产生的日志项。这个日志项包含前缀、日期时间戳、该日志具体是由哪个源文件记录的、源文件记录日志所在行,最后是日志消息。让我们看一下如何配置 log
包来输出这样的日志项,如代码清单8-3所示。
代码清单8-3 listing03.go
01 // 这个示例程序展示如何使用最基本的log包
02 package main
03
04 import (
05 "log"
06 )
07
08 func init() {
09 log.SetPrefix("TRACE: ")
10 log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
11 }
12
13 func main() {
14 // Println写到标准日志记录器
15 log.Println("message")
16
17 // Fatalln在调用Println()之后会接着调用os.Exit(1)
18 log.Fatalln("fatal message")
19
20 // Panicln在调用Println()之后会接着调用panic()
21 log.Panicln("panic message")
22 }
如果执行代码清单8-3中的程序,输出的结果会和代码清单8-2所示的输出类似。让我们分析一下代码清单8-4中的代码,看看它是如何工作的。
代码清单8-4 listing03.go:第08行到第11行
08 func init() {
09 log.SetPrefix("TRACE: ")
10 log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
11 }
在第08行到第11行,定义的函数名为 init()
。这个函数会在运行 main()
之前作为程序初始化的一部分执行。通常程序会在这个 init()
函数里配置日志参数,这样程序一开始就能使用 log
包进行正确的输出。在这段程序的第9行,设置了一个字符串,作为每个日志项的前缀。这个字符串应该是能让用户从一般的程序输出中分辨出日志的字符串。传统上这个字符串的字符会全部大写。
有几个和 log
包相关联的标志,这些标志用来控制可以写到每个日志项的其他信息。代码清单8-5展示了目前包含的所有标志。
代码清单8-5 golang.org/src/log/log.go
const (
// 将下面的位使用或运算符连接在一起,可以控制要输出的信息。没有
// 办法控制这些信息出现的顺序(下面会给出顺序)或者打印的格式
// (格式在注释里描述)。这些项后面会有一个冒号:
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
// 日期: 2009/01/23
Ldate = 1 << iota
// 时间: 01:23:23
Ltime
// 毫秒级时间: 01:23:23.123123。该设置会覆盖Ltime标志
Lmicroseconds
// 完整路径的文件名和行号: /a/b/c/d.go:23
Llongfile
// 最终的文件名元素和行号: d.go:23
// 覆盖 Llongfile
Lshortfile
// 标准日志记录器的初始值
LstdFlags = Ldate | Ltime
)
代码清单8-5是从 log
包里直接摘抄的源代码。这些标志被声明为常量,这个代码块中的第一个常量叫作 Ldate
,使用了特殊的语法来声明,如代码清单8-6所示。
代码清单8-6 声明 Ldate
常量
// 日期: 2009/01/23
Ldate = 1 << iota
关键字 iota
在常量声明区里有特殊的作用。这个关键字让编译器为每个常量复制相同的表达式,直到声明区结束,或者遇到一个新的赋值语句。关键字 iota
的另一个功能是, iota
的初始值为0,之后 iota
的值在每次处理为常量后,都会自增1。让我们更仔细地看一下这个关键字,如代码清单8-7所示。
代码清单8-7 使用关键字 iota
const (
Ldate = 1 << iota // 1 << 0 = 000000001 = 1
Ltime // 1 << 1 = 000000010 = 2
Lmicroseconds // 1 << 2 = 000000100 = 4
Llongfile // 1 << 3 = 000001000 = 8
Lshortfile // 1 << 4 = 000010000 = 16
...
)
代码清单8-7展示了常量声明背后的处理方法。操作符 <<
对左边的操作数执行按位左移操作。在每个常量声明时,都将1按位左移 iota
个位置。最终的效果使为每个常量赋予一个独立位置的位,这正好是标志希望的工作方式。
常量 LstdFlags
展示了如何使用这些标志,如代码清单8-8所示。
代码清单8-8 声明 LstdFlags
常量
const (
...
LstdFlags = Ldate(1) | Ltime(2) = 00000011 = 3
)
在代码清单8-8中看到,因为使用了复制操作符, LstdFlags
打破了 iota
常数链。由于有 |
运算符用于执行或操作,常量 LstdFlags
被赋值为3。对位进行或操作等同于将每个位置的位组合在一起,作为最终的值。如果对位1和2进行或操作,最终的结果就是3。
让我们看一下我们要如何设置日志标志,如代码清单8-9所示。
代码清单8-9 listing03.go:第08行到第11行
08 func init() {
09 ...
10 log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
11 }
这里我们将 Ldate
、 Lmicroseconds
和 Llongfile
标志组合在一起,将该操作的值传入 SetFlags
函数。这些标志值组合在一起后,最终的值是 13
,代表第1、3和4位为1(00001101)。由于每个常量表示单独一个位,这些标志经过或操作组合后的值,可以表示每个需要的日志参数。之后 log
包会按位检查这个传入的整数值,按照需求设置日志项记录的信息。
初始完 log
包后,可以看一下 main()
函数,看它是如何写消息的,如代码清单8-10所示。
代码清单8-10 listing03.go:第13行到第22行
13 func main() {
14 // Println写到标准日志记录器
15 log.Println("message")
16
17 // Fatalln在调用Println()之后会接着调用os.Exit(1)
18 log.Fatalln("fatal message")
19
20 // Panicln在调用Println()之后会接着调用panic()
21 log.Panicln("panic message")
22 }
代码清单8-10展示了如何使用3个函数 Println
、 Fatalln
和 Panicln
来写日志消息。这些函数也有可以格式化消息的版本,只需要用 f
替换结尾的 ln
。 Fatal
系列函数用来写日志消息,然后使用 os.Exit(1)
终止程序。 Panic
系列函数用来写日志消息,然后触发一个 panic
。除非程序执行 recover
函数,否则会导致程序打印调用栈后终止。 Print
系列函数是写日志消息的标准方法。
log
包有一个很方便的地方就是,这些日志记录器是多goroutine安全的。这意味着在多个goroutine可以同时调用来自同一个日志记录器的这些函数,而不会有彼此间的写冲突。标准日志记录器具有这一性质,用户定制的日志记录器也应该满足这一性质。
现在知道了如何使用和配置 log
包,让我们看一下如何创建一个定制的日志记录器,以便可以让不同等级的日志写到不同的目的地。